#!/usr/bin/env python3
import argparse
import base64
import hashlib
import os.path
import shutil
import tempfile
import zipfile
from typing import NamedTuple


class Wheel(NamedTuple):
    src: str
    plat: str
    exe: str = 'sentry-cli'


WHEELS = (
    Wheel(
        src='sentry-cli-Darwin-arm64',
        plat='macosx_11_0_arm64',
    ),
    Wheel(
        src='sentry-cli-Darwin-universal',
        plat='macosx_11_0_universal2',
    ),
    Wheel(
        src='sentry-cli-Darwin-x86_64',
        plat='macosx_10_15_x86_64',
    ),
    Wheel(
        src='sentry-cli-Linux-aarch64',
        plat='manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_2_aarch64',
    ),
    Wheel(
        src='sentry-cli-Linux-armv7',
        plat='manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_2_armv7l',
    ),
    Wheel(
        src='sentry-cli-Linux-i686',
        plat='manylinux_2_17_i686.manylinux2014_i686.musllinux_1_2_i686',
    ),
    Wheel(
        src='sentry-cli-Linux-x86_64',
        plat='manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_2_x86_64',
    ),
    Wheel(
        src='sentry-cli-Windows-i686.exe',
        plat='win32',
        exe='sentry-cli.exe',
    ),
    Wheel(
        src='sentry-cli-Windows-x86_64.exe',
        plat='win_amd64',
        exe='sentry-cli.exe',
    ),
    Wheel(
        src='sentry-cli-Windows-aarch64.exe',
        plat='win_arm64',
        exe='sentry-cli.exe',
    ),
)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument('--binaries', required=True)
    parser.add_argument('--base', required=True)
    parser.add_argument('--dest', required=True)
    args = parser.parse_args()

    expected = {wheel.src for wheel in WHEELS}
    received = set(os.listdir(args.binaries))
    if expected < received:
        raise SystemExit(
            f'Unexpected binaries:\n\n'
            f'- extra: {", ".join(sorted(received - expected))}\n'
            f'- missing: {", ".join(sorted(expected - received))}'
        )

    sdist_path = wheel_path = None
    for fname in os.listdir(args.base):
        if fname.endswith('.tar.gz'):
            sdist_path = os.path.join(args.base, fname)
        elif fname.endswith('.whl'):
            wheel_path = os.path.join(args.base, fname)
        else:
            raise SystemExit(f'unexpected file in `--base`: {fname}')

    if sdist_path is None or wheel_path is None:
        raise SystemExit('expected wheel and sdist in `--base`')

    os.makedirs(args.dest, exist_ok=True)
    shutil.copy(sdist_path, args.dest)

    for wheel in WHEELS:
        binary_src = os.path.join(args.binaries, wheel.src)
        binary_size = os.stat(binary_src).st_size
        with open(binary_src, 'rb') as bf:
            digest = hashlib.sha256(bf.read()).digest()
            digest_b64 = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()

        basename = os.path.basename(wheel_path)
        wheelname, _ = os.path.splitext(basename)
        name, version, py, abi, plat = wheelname.split('-')

        with tempfile.TemporaryDirectory() as tmp:
            with zipfile.ZipFile(wheel_path) as zipf:
                zipf.extractall(tmp)

            distinfo = os.path.join(tmp, f'{name}-{version}.dist-info')
            scripts = os.path.join(tmp, f'{name}-{version}.data', 'scripts')

            # replace the script binary with our copy
            os.remove(os.path.join(scripts, 'sentry-cli'))
            shutil.copy(binary_src, os.path.join(scripts, wheel.exe))

            # rewrite RECORD to include the new file
            record_fname = os.path.join(distinfo, 'RECORD')
            with open(record_fname) as f:
                record_lines = list(f)

            record = f'{name}-{version}.data/scripts/sentry-cli,'
            for i, line in enumerate(record_lines):
                if line.startswith(record):
                    record_lines[i] = (
                        f'{name}-{version}.data/scripts/{wheel.exe},'
                        f'sha256={digest_b64},'
                        f'{binary_size}\n'
                    )
                    break
            else:
                raise SystemExit(f'could not find {record!r} in RECORD')

            with open(record_fname, 'w') as f:
                f.writelines(record_lines)

            # rewrite WHEEL to have the new tags
            wheel_fname = os.path.join(distinfo, 'WHEEL')
            with open(wheel_fname) as f:
                wheel_lines = list(f)

            for i, line in enumerate(wheel_lines):
                if line.startswith('Tag: '):
                    wheel_lines[i:i + 1] = [
                        f'Tag: {py}-{abi}-{plat}\n'
                        for plat in wheel.plat.split('.')
                    ]
                    break
            else:
                raise SystemExit("could not find 'Tag: ' in WHEEL")

            with open(wheel_fname, 'w') as f:
                f.writelines(wheel_lines)

            # write out the final zip
            new_basename = f'{name}-{version}-{py}-{abi}-{wheel.plat}.whl'
            tmp_new_wheel = os.path.join(tmp, new_basename)
            fnames = sorted(
                os.path.join(root, fname)
                for root, _, fnames in os.walk(tmp)
                for fname in fnames
            )
            with zipfile.ZipFile(tmp_new_wheel, 'w') as zipf:
                for fname in fnames:
                    zinfo = zipfile.ZipInfo(os.path.relpath(fname, tmp))
                    if '/scripts/' in zinfo.filename:
                        zinfo.external_attr = 0o100755 << 16
                    with open(fname, 'rb') as fb:
                        zipf.writestr(zinfo, fb.read())

            # move into dest
            shutil.move(tmp_new_wheel, args.dest)

    return 0


if __name__ == '__main__':
    raise SystemExit(main())
